package relay

import (
	"context"
	"feishup2pclient/p2p"
	"fmt"
	"sync"
	"time"

	"github.com/libp2p/go-libp2p"
	"github.com/libp2p/go-libp2p-core/crypto"
	"github.com/libp2p/go-libp2p-core/host"
	"github.com/libp2p/go-libp2p-core/network"
	"github.com/libp2p/go-libp2p-core/peer"
	"github.com/libp2p/go-libp2p-core/protocol"
	"github.com/libp2p/go-libp2p-core/routing"
	dht "github.com/libp2p/go-libp2p-kad-dht"
	ma "github.com/multiformats/go-multiaddr"
)

func listen_addrs(port int) []string {
	addrs := []string{
		"/ip4/0.0.0.0/tcp/%d",
		"/ip4/0.0.0.0/udp/%d/quic",
		"/ip6/::/tcp/%d",
		"/ip6/::/udp/%d/quic",
	}

	for i, a := range addrs {
		addrs[i] = fmt.Sprintf(a, port)
	}

	return addrs
}

func bootstrap(ctx context.Context, h host.Host) {

	// Convert Bootstap Nodes into usable addresses.
	BootstrapPeers := make(map[peer.ID]*peer.AddrInfo, len(p2p.Peers))
	for _, addrStr := range p2p.Peers {
		addr, err := ma.NewMultiaddr(addrStr)
		if err != nil {
			continue
		}
		pii, err := peer.AddrInfoFromP2pAddr(addr)
		if err != nil {
			continue
		}
		pi, ok := BootstrapPeers[pii.ID]
		if !ok {
			pi = &peer.AddrInfo{ID: pii.ID}
			BootstrapPeers[pi.ID] = pi
		}
		pi.Addrs = append(pi.Addrs, pii.Addrs...)
	}

	bootstrap := func() {
		w := sync.WaitGroup{}
		success := 0
		for _, p := range BootstrapPeers {
			w.Add(1)
			go func(p peer.AddrInfo) {
				defer w.Done()
				if err := h.Connect(ctx, p); err == nil {
					success += 1
					fmt.Printf("connect bootstrap node %s success", p.ID)
					fmt.Println()
				} else {
					fmt.Printf("connect bootstrap node %s fail: %s", p.ID, err)
					fmt.Println()
				}
			}(*p)
		}

		w.Wait()

		if success == 0 {
			// panic("bootstrap fail")
		}
	}

	//re-bootstrap after network recovery
	for {
		if len(h.Network().Conns()) < 1 {
			bootstrap()
		}

		select {
		case <-time.After(5 * time.Minute):
		case <-ctx.Done():
			return
		}
	}
}

func NewServerNode(ctx context.Context, port int, privKey crypto.PrivKey) (host.Host, error) {

	h, err := libp2p.New(
		libp2p.ListenAddrStrings(listen_addrs(port)...),
		libp2p.Identity(privKey),
		libp2p.Routing(func(h host.Host) (routing.PeerRouting, error) {
			return dht.New(ctx, h, dht.Mode(dht.ModeClient))
		}),
		libp2p.DisableRelay(),
		// libp2p.re
	)

	if err != nil {
		return nil, err
	}

	h.SetStreamHandler(protocol.TestingID, func(s network.Stream) {
		buf := make([]byte, 5)
		n, err := s.Read(buf)
		if err == nil && string(buf[:n]) == "hello" {
			s.Write([]byte("world"))
		}

	})

	go bootstrap(ctx, h)

	return h, nil
}

func NewClientNode(ctx context.Context, port int, privKey crypto.PrivKey) (host.Host, error) {

	h, err := libp2p.New(
		libp2p.Identity(privKey),
		libp2p.ListenAddrStrings(listen_addrs(port)...),
		libp2p.Routing(func(h host.Host) (routing.PeerRouting, error) {
			return dht.New(ctx, h, dht.Mode(dht.ModeClient))
		}),
		libp2p.EnableRelay(),
	)
	if err != nil {
		return nil, err
	}

	go bootstrap(ctx, h)

	return h, nil
}
